<?php
// +----------------------------------------------------------------------
// | INPHP
// | Copyright (c) 2023 https://inphp.cc All rights reserved.
// | Licensed ( https://opensource.org/licenses/MIT )
// | Author: 幺月儿(https://gitee.com/lulanyin) Email: inphp@qq.com
// +----------------------------------------------------------------------
// | 文件上传处理
// +----------------------------------------------------------------------
namespace app\fs\http\api;

use app\admin\attributes\auth;
use Inphp\Core\Config;
use Inphp\Core\Context;
use Inphp\Core\Db\Db;
use Inphp\Core\Object\Message;
use Inphp\Core\Util\Str;
use Intervention\Image\ImageManagerStatic as Image;

#[auth] class upload
{
    /**
     * UID
     * @var int
     */
    public int $uid = 0;

    /**
     * 配置
     * @var array
     */
    private array $config = [
        //最大接受的文件大小，1G
        "maxSize"       => 1024 * 1024 * 1024,
        //如果是图片
        "image"         => [
            //最大宽度
            "maxWidth"          => 2560,
            //最大高度
            "maxHeight"         => 2560,
            //水印
            "watermark"         => "",
            //图片水印位置，1~9可选
            "watermarkPosition" => 9
        ],
        //文件后缀对应的文件类型，其它统一为 file
        "extensions"    => [
            //图片
            "image"     => "jpg,png,jpeg,bmp,gif,webp",
            //视频
            "video"     => "mp4,avi,mov,flv,wmv,mkv"
        ]
    ];

    /**
     * 默认保存的根目录
     * @var string
     */
    private string $attachmentDir = "";

    /**
     * 上传已处理的文件列表
     * @var array
     */
    public array $list = [];

    /**
     * 本次上传指定的父级目录
     * @var int
     */
    public int $parentId = 0;

    /**
     * 分组名称
     * @var string
     */
    public string $groupName = "other";

    /**
     * 重设比例参数
     * @var string
     */
    private string $resize = "";

    /**
     * 初始化配置
     */
    public function __construct()
    {
        $config = Config::get("module.fs");
        $config = is_array($config) ? $config : [];
        //覆盖配置
        $this->config = array_merge($this->config, $config);
        $this->attachmentDir = Config::get("define.attachment");
    }

    /**
     * 默认上传入口
     * @return Message
     */
    public function index(): Message
    {
        //父级ID
        $parentId = POST("parentId", 0);
        $this->parentId = is_numeric($parentId) && $parentId > 0 ? ceil($parentId) : 0;
        //分组名称，只支持字母或数字开头可在中间加下划线结尾必须是字母或数字
        $groupName = POST("groupName", "other");
        $groupName = !empty($groupName) ? Str::trim($groupName) : "other";
        $this->groupName = preg_match("/^[a-z0-9][a-z0-9_]*[a-z0-9]$/i", $groupName) > 0 ? $groupName : "other";
        //重设图片大小的参数
        $resize = POST("resize");
        $this->resize = !empty($resize) ? Str::trim($resize) : "";
        //处理$_FILES用
        $client = Context::getClient();
        //处理POST的base64数据
        $base64 = POST("base64");
        //只能用一个方式上传文件
        if (!empty($base64)) {
            //文件名称
            $fileName = POST("fileName");
            $fileName = !empty($fileName) ? trim(Str::trim($fileName, " ")) : null;
            if (is_null($fileName)) {
                return httpMessage("缺少文件名");
            }
            if (in_array(strtolower(strrchr($fileName, ".")), [".html", ".htm", ".php", ".js"])) {
                return httpMessage("禁止上传该类文件");
            }
            //大小限制
            if (strlen($base64) > $this->config["maxSize"] && $this->config["maxSize"] > 0) {
                return httpMessage("超出文件大小限制");
            }
            if(stripos($base64, 'data:')===0 && stripos($base64, 'base64,')>0){
                $base64 = str_replace(" ", "+", $base64);
                $base64 = explode('base64,', $base64, 2)[1];
                $base64 = @base64_decode($base64) ?? null;
                if (empty($base64)) {
                    return httpMessage("没有上传数据");
                }
                //当前上传的是第几部分
                $part = POST("part", 1);
                $part = is_numeric($part) && $part > 0 ? ceil($part) : 1;
                //共几部分
                $parts = POST("parts", 1);
                $parts = is_numeric($parts) && $parts > 0 ? ceil($parts) : 1;
                //开始处理
                $this->base64($base64, $fileName, $parts, $part);
            } else {
                return httpMessage("文件数据格式错误");
            }
        } elseif (!empty($client->files)) {
            //$this->files($client->files); //未测试验证的功能，暂不执行
            return httpMessage("暂不提供表单文件上传处理");
        }
        //如果为空
        if (empty($this->list)) {
            return httpMessage("并未上传任何文件数据");
        }
        return httpMessage(0, "success", $this->list);
    }

    /**
     * 处理 $_FILES (未测试验证的功能)
     * @param array $files
     */
    private function files(array $files)
    {
        foreach ($files as $file) {
            //新文件
            $error = $file["error"] ?? 0;
            $message = null;
            //错误状态
            if ($error) {
                $message = "文件错误，无法处理";
            } elseif ($file["size"] > $this->config["maxSize"] && $this->config["maxSize"] > 0) {
                $message = "文件大小超出限制，已忽略";
            }
            if (!is_null($message)) {
                $this->list[] = compact("error", "message");
            } else {
                //处理文件保存
                $this->list[] = $this->processFile($this->saveFile($file));
            }
        }
    }

    /**
     * 保存POST的$_FILES文件 (未测试验证的功能)
     * @param array $file
     * @return array
     */
    public function saveFile(array $file): array
    {
        //拓展
        $extension = strtolower(strrchr($file["name"], "."));
        $extension = $extension ?? "";
        $extension = !empty($extension) ? substr($extension, 1) : "";
        //文件名称处理，用于保存到数据库的
        $fileName = mb_strlen($file["name"]) > 50 ? mb_substr($file["name"], 0, 50) : $file["name"];
        //获取类型
        $type = $this->matchType($extension);
        //保存路径，按日期保存吧
        $dir = "/{$type}s/".date("Y/m/d");
        //返回数据
        $item = [
            "error"     => 0,
            "message"   => "success",
            "name"      => $file["name"],
            "type"      => $type,
            "extension" => $extension,
            "exists"    => 0
        ];
        //判断文件夹是否已存在
        if (!is_dir($this->attachmentDir.$dir)) {
            if (!@mkdir($this->attachmentDir.$dir, 0777, true)) {
                return array_merge($item, [
                    "error"     => 1,
                    "message"   => "无法创建文件目录，无权限！"
                ]);
            }
        }
        //开始处理临时文件转移
        if (is_file($file["tmp_name"])) {
            //md5
            $hash = md5_file($file["tmp_name"]);
            //检测文件是否重复
            $existsFile = $this->exists($hash, $fileName);
            if (!empty($existsFile)) {
                $item = array_merge($item, [
                    "id"        => $existsFile["id"],
                    "src"       => $existsFile["src"],
                    "exists"    => 1
                ]);
            } else {
                //未存在同样文件
                //使用新的文件名保存到服务端附件目录
                $baseName = !empty($extension) ? join(".", [$hash, $extension]) : $hash;
                $size = round(filesize($file["tmp_name"]) / 1024, 2);
                if (move_uploaded_file($file["tmp_name"], $this->attachmentDir.$dir."/".$baseName)) {
                    //将文件保存到数据库
                    $db = Db::from("fs_files")
                        ->set([
                            "parentId"  => $this->parentId,
                            "groupName" => $this->groupName,
                            "name"      => $fileName,
                            "extension" => $extension,
                            "src"       => $dir."/".$baseName,
                            "hash"      => $hash,
                            "ip"        => getIP(),
                            "type"      => $type,
                            "uid"       => $this->uid,
                            "size"      => $size
                        ]);
                    if ($db->insert()) {
                        $item = array_merge($item, [
                            "id"    => $db->getLastInsertId(),
                            "src"   => $dir."/".$baseName
                        ]);
                    }
                } else {
                    $item["error"] = 1;
                    $item["message"] = "无法保存文件";
                }
            }
        } else {
            $item["error"] = 1;
            $item["message"] = "临时文件为空，文件上传失败";
        }
        return $item;
    }

    /**
     * 处理base64
     * @param string $data
     * @param string $fileName
     * @param int $parts
     * @param int $part
     */
    public function base64(string $data, string $fileName, int $parts = 1, int $part = 1)
    {
        $extension = strtolower(strrchr($fileName, "."));
        $extension = $extension ?? "";
        $extension = !empty($extension) ? substr($extension, 1) : "";
        //获取类型
        $type = $this->matchType($extension);
        //保存路径，按日期保存吧
        $dir = "/{$type}s/".date("Y/m/d");
        if (!is_dir($this->attachmentDir.$dir)) {
            if (!@mkdir($this->attachmentDir.$dir, 0777, true)) {
                $this->list = [
                    "error"     => 1,
                    "message"   => "无法创建文件目录，无权限！"
                ];
                return;
            }
        }
        //临时文件名称处理
        if ($part === 1) {
            //第一片，都需要由服务端生成临时文件名
            $tmpFileName = $dir."/".$this->newFileName($this->attachmentDir.$dir."/", !empty($extension) ? ".{$extension}" : "", true);
        } else {
            //由客户端指定临时文件名
            $tmpFileName = POST("tmpName");
            $tmpFileName = !empty($tmpFileName) ? $tmpFileName : null;
            if (empty($tmpFileName) || strrchr($tmpFileName, ".tmp") !== ".tmp") {
                $this->list = [
                    "error"     => 1,
                    "message"   => "临时文件未协商"
                ];
                return;
            }
            //检测临时文件是否存在
            if (!is_file($this->attachmentDir . $tmpFileName)) {
                $this->list = [
                    "error"     => 1,
                    "message"   => "临时文件无效，中止处理"
                ];
                return;
            }
            //文件大小判断
            if ($this->config["maxSize"] > 0 && (filesize($this->attachmentDir . $tmpFileName) + strlen($data)) > $this->config["maxSize"]) {
                $this->list = [
                    "error"     => 1,
                    "message"   => "文件大小已超出限制，已作废"
                ];
                //删除临时文件
                @unlink($this->attachmentDir . $tmpFileName);
                return;
            }
        }
        //组装文件路径
        $tmpFile = $this->attachmentDir . $tmpFileName;
        //持续写入
        $res = fopen($tmpFile, "a");
        if ($res && fwrite($res, $data)) {
            // 1 < 1+1 2 < 1+1 2 < 2+1
            if ($parts === 1 || $parts < $part + 1) {
                //上传完成了
                $hash = md5_file($tmpFile);
                $size = round(filesize($tmpFile) / 1024, 2);
                //使用时间 + 5位随机数 作为保存的文件名称
                $baseName = $this->newFileName($this->attachmentDir.$dir."/", !empty($extension) ? ".{$extension}" : "");
                //构建要返回的文件数据
                $item = [
                    "id"        => 0,
                    "error"     => 0,
                    "message"   => "success",
                    "name"      => $fileName,
                    "type"      => $type,
                    "extension" => $extension,
                    "exists"    => 0,
                    "src"       => $dir."/".$baseName,
                    "hash"      => $hash,
                    "size"      => $size
                ];
                if (rename($tmpFile, $this->attachmentDir.$item["src"])) {
                    //判断是否存在相同hash值的文件
                    $existsFile = $this->exists($hash, $fileName);
                    if (!empty($existsFile)) {
                        //如果存在，则直接移除现有文件
                        @unlink($this->attachmentDir.$item["src"]);
                        //取旧文件
                        $item["id"]     = $existsFile["id"];
                        $item["src"]    = $existsFile["src"];
                        $item["exists"] = 1;
                    } else {
                        //不存在相同文件，保存文件到数据库
                        //文件名称过长处理，用于保存到数据库的
                        $fileName = mb_strlen($fileName) > 100 ? mb_substr($fileName, 0, 100) : $fileName;
                        //将文件保存到数据库
                        $db = Db::from("fs_files")
                            ->set([
                                "parentId"  => $this->parentId,
                                "groupName" => $this->groupName,
                                "name"      => $fileName,
                                "extension" => $extension,
                                "src"       => $item["src"],
                                "hash"      => $hash,
                                "ip"        => getIP(),
                                "type"      => $type,
                                "uid"       => $this->uid,
                                "size"      => $item["size"]
                            ]);
                        if ($db->insert()) {
                            //获得文件ID
                            $item["id"] = $db->getLastInsertId();
                        }
                    }
                    //关闭文件
                    @fclose($res);
                } else {
                    $item["error"] = 1;
                    $item["message"] = "无法重命名文件";
                    //关闭文件
                    @fclose($res);
                    //删除临时文件
                    @unlink($tmpFile);
                }
                //上传成功
                $this->list = $this->processFile($item);
            } else {
                //当前部分上传完成
                $this->list = [
                    "error"     => 0,
                    "type"      => $type,
                    "name"      => $fileName,
                    "message"   => "上传{$part}/{$parts}完成",
                    //返回临时文件名
                    "tmpName"   => $tmpFileName
                ];
                //关闭文件
                @fclose($res);
            }
        } else {
            $this->list = [
                "error"     => 1,
                "type"      => $type,
                "name"      => $fileName,
                "tmpName"   => $tmpFileName,
                "message"   => "文件写入失败"
            ];
            //关闭文件
            @fclose($res);
            //删除临时文件
            @unlink($tmpFile);
        }
    }

    /**
     * 生成不重复的文件名称
     * @param string $dir
     * @param string $extension
     * @param bool $tmp
     * @return string
     */
    private function newFileName(string $dir, string $extension, bool $tmp = false): string
    {
        $int = time() . mt_rand(10000, 99999);
        if (is_file($dir . $int . $extension . ($tmp ? ".tmp" : ""))) {
            return $this->newFileName($dir, $extension, $tmp);
        }
        return $int . $extension . ($tmp ? ".tmp" : "");
    }

    /**
     * 检测是否重复
     * @param string $hash
     * @param string $fileName
     * @return array
     */
    private function exists(string $hash, string $fileName): array
    {
        //检测同文件夹下，是否存在同样的文件
        $exists = Db::from("fs_files")
            ->where("type", "!=", "dir")
            ->where("hash", $hash)
            ->where("parentId", $this->parentId)
            ->where("groupName", $this->groupName)
            ->first();
        //检测文件是否存在，如果文件为空或者文件不存在
        if (empty($exists) || !is_file($this->attachmentDir . $exists["src"])) {
            //如果不存在，再使用hash检查
            $exists = Db::from("fs_files")
                ->where("type", "!=", "dir")
                ->where("hash", $hash)
                ->first();
            if (empty($exists) || !is_file($this->attachmentDir . $exists["src"])) {
                //不存在
                return [];
            }
            //保存一份到当前文件夹
            $copy = [
                "parentId"  => $this->parentId,
                "groupName" => $this->groupName,
                "name"      => $fileName,
                "extension" => $exists["extension"],
                "src"       => $exists["src"],
                "hash"      => $exists["hash"],
                "ip"        => getIP(),
                "type"      => $exists["type"],
                "uid"       => $this->uid,
                "size"      => $exists["size"]
            ];
            $db = Db::from("fs_files")->set($copy);
            if ($db->insert()) {
                $exists["id"] = $db->getLastInsertId();
                $exists["destroy"] = 0;
            }
        }
        if ($exists["destroy"] == 1) {
            //恢复文件
            Db::from("fs_files")->where("id", $exists["id"])->update(["destroy" => 0]);
        }
        return $exists;
    }

    /**
     * 处理文件
     * @param array $file
     * @return array
     */
    private function processFile(array $file): array
    {
        if ($file["error"] === 0 && is_file($this->attachmentDir.$file["src"])) {
            //如果是图片文件，需要自动压缩处理
            if ($file["type"] === "image" && class_exists('Intervention\Image\ImageManagerStatic')) {
                //方式
                $type = "resize";
                $percent = 0;
                $cropWidth = 0;
                $cropHeight = 0;
                //如果客户端指定了重设大小的参数
                $maxWidth = $this->config["image"]["maxWidth"];
                $maxHeight = $this->config["image"]["maxHeight"];
                $this->resize = !empty($this->resize) ? $this->resize : "max{$maxWidth}:max{$maxHeight}";
                if (!empty($this->resize)) {
                    if (stripos($this->resize, ":") > 0) {
                        $values = explode(":", $this->resize);
                        if (is_numeric($values[0]) && $values[0] > 0 && is_numeric($values[1]) && $values[1] > 0) {
                            // 1:2 .... 这类值，计算出比例值即可
                            $percent = $values[0] / $values[1];
                        } elseif (stripos($values[0], "max") === 0 && stripos($values[1], "max") === 0) {
                            //max2560:max2560
                            $width = substr($values[0], 3);
                            $width = is_numeric($width) && $width > 0 ? ceil($width) : 0;
                            $height = substr($values[1], 3);
                            $height = is_numeric($height) && $height > 0 ? ceil($height) : 0;
                            if ($width > 0 && $height > 0) {
                                $maxWidth = $width;
                                $maxHeight = $height;
                            }
                        }
                    } elseif (stripos($this->resize, "x") > 0) {
                        //600x600 ... 固定值，用于直接裁剪
                        $values = explode("x", $this->resize);
                        if (is_numeric($values[0]) && $values[0] > 0 && is_numeric($values[1]) && $values[1] > 0) {
                            $cropWidth = ceil($values[0]);
                            $cropHeight = ceil($values[1]);
                            //方式变成裁剪
                            $type = "crop";
                        }
                    } elseif (is_numeric($this->resize) && $this->resize > 0) {
                        //按给定的比例值等比裁剪
                        $percent = $this->resize;
                    }
                }
                $percent = $percent > 0 ? round($percent, 2) : 0;
                //首先按比例裁剪
                if ($percent > 0) {
                    $this->crop($file, $percent);
                }
                //如果是裁剪
                if ($type == "crop" && $cropWidth > 0 && $cropHeight > 0) {
                    //裁剪为指定大小
                    $this->crop($file, $cropWidth, $cropHeight);
                } elseif ($percent > 0) {
                    //裁剪到指定比例
                    $this->crop($file, $percent);
                } else {
                    //按限定大小处理
                    $image = Image::make($this->attachmentDir.$file["src"]);
                    $width = $image->getWidth();
                    $height = $image->getHeight();
                    if ($width > $maxWidth || $height > $maxHeight) {
                        //超出画面，等比缩小即可
                        if ($width > $maxWidth) {
                            $height = ceil($height * ($maxWidth / $width));
                            $width = $maxWidth;
                        }
                        if ($height > $maxHeight) {
                            $width = ceil($width * ($maxHeight / $height));
                            $height = $maxHeight;
                        }
                        $image->resize($width, $height);
                        if ($file["exists"] == 0) {
                            //文件并非已存在，直接修改保存即可
                            $image->save();
                        } else {
                            //需要重新保存一条数据
                            //因为文件已存在，为避免其它地方使用了原尺寸图片
                            $dir = "/images/".date("Y/m/d/");
                            if (!is_dir($this->attachmentDir.$dir)) {
                                @mkdir($this->attachmentDir.$dir, 0777, true);
                            }
                            $baseName = $this->newFileName($this->attachmentDir.$dir, $file["extension"]);
                            $file["src"] = $dir.$baseName;
                            $image->save($this->attachmentDir.$file["src"]);
                        }
                        //
                        $newHash = md5_file($this->attachmentDir.$file["src"]);
                        $this->updateFile($file, $newHash);
                    }
                }
            }
            //处理为全路径
            $file["url"] = attachmentUrl($file["src"]);
        }
        return $file;
    }

    /**
     * 根据比例等比缩放后裁剪
     * @param array $file
     * @param float|int $width
     * @param ?int $height
     */
    private function crop(array &$file, float|int $width = 0, ?int $height = null)
    {
        $image = Image::make($this->attachmentDir.$file["src"]);
        $imageWidth = $image->getWidth();
        $imageHeight = $image->getHeight();
        if (is_null($height)) {
            $percent = $width;
        } else {
            $percent = $width / $height;
        }
        //是否存在同样文件
        $exists = $file["exists"] ?? 0;
        if (round($imageWidth / $imageHeight, 2) != $percent) {
            //按最小边取宽度
            $maxWidth = min($imageWidth, $imageHeight);
            //再求高度
            $maxHeight = floor($maxWidth / $percent);
            //然后取x和y的标
            $posX = floor(($imageWidth - $maxWidth) / 2);
            $posY = floor(($imageHeight - $maxHeight) / 2);
            //裁剪
            $image->crop($maxWidth, $maxHeight, $posX, $posY);
            if ($exists == 0) {
                $image->save();
            } else {
                //因为文件已存在，为避免其它地方使用了原尺寸图片
                $dir = "/images/".date("Y/m/d");
                if (!is_dir($this->attachmentDir.$dir)) {
                    @mkdir($this->attachmentDir.$dir, 0777, true);
                }
                $baseName = time().rand(0, 999)."_{$maxWidth}x{$maxHeight}.{$file["extension"]}";
                $file["src"] = $dir."/".$baseName;
                $image->save($this->attachmentDir.$file["src"]);
            }
        }
        //如果指定了宽度
        if (!is_null($height)) {
            //重设大小即可
            $image = Image::make($this->attachmentDir.$file["src"]);
            $image->resize($width, $height);
            $image->save();
        }
        $hash = md5_file($this->attachmentDir.$file["src"]);
        //重新保存文件
        $this->updateFile($file, $hash);
    }

    /**
     * 重新保存文件
     * @param array $file
     * @param string $newHash
     */
    private function updateFile(array &$file, string $newHash) {
        $newSize = round(filesize($this->attachmentDir.$file["src"]) / 1024, 2);
        if ($file["exists"] == 0) {
            if ($newHash != $file["hash"]) {
                @Db::from("fs_files")
                    ->where("id", $file["id"])
                    ->update([
                        "hash"      => $newHash,
                        "destroy"   => 0,
                        "size"      => $newSize
                    ]);
            }
            $file["hash"] = $newHash;
        } else {
            //重新保存一条数据
            $file["hash"] = $newHash;
            $db = Db::from("fs_files")
                ->set([
                    "parentId"  => $this->parentId,
                    "groupName" => $this->groupName,
                    "name"      => $file["name"],
                    "extension" => $file["extension"],
                    "src"       => $file["src"],
                    "hash"      => $file["hash"],
                    "ip"        => getIP(),
                    "type"      => "image",
                    "uid"       => $this->uid,
                    "size"      => $newSize
                ]);
            if ($db->insert()) {
                $file["id"] = $db->getLastInsertId();
            }
        }
        //新文件，不存在重复
        $file["exists"] = 0;
    }

    /**
     * 根据后缀处理文件类型
     * @param string $extension
     * @return string
     */
    private function matchType(string $extension): string
    {
        $extension = stripos($extension, ".") === 0 ? substr($extension, 1) : $extension;
        if (!empty($extension)) {
            $extensions = $this->config["extensions"] ?? [];
            $extensions = is_array($extensions) ? $extensions : [];
            foreach ($extensions as $name => $list) {
                $list = is_array($list) ? $list : explode(",", Str::trim($list));
                if (in_array($extension, $list)) {
                    return $name;
                }
            }
        }
        return "file";
    }
}